iOS笔记梳理
App启动
1.有storyboard
1.main函数;2.创建UIApplication对象;3.根据Info.plist获取Main.storyboard加载;4.创建UIWindow;
2.无storyboard
1.main函数 2.创建UiApplication对象; 3.创建UIApplicationd的delegate对象;4.delegate对象开始处理系统事件,调用代理;5.执行到application:didFinishLaunchingWithOptions;6.创建UIWindow;7.设置RootViewcontroller; 8.显示窗口;
启动流程
- 区分冷热启动,这里主要讲冷启动
1.main函数执行前
1.加载可执行文件。(App里的所有.o文件)
2.加载动态链接库,进行rebase指针调整和bind符号绑定
3.objc的runtime初始化 包括:objc相关Class的注册、category注册、selector唯一性检查等
4.初始化。 包括:执行+load()方法、用C++静态构造器 attribute((constructor))修饰的函数的调用、创建C++静态全局变量等
简单来说,
App启动后,首先,系统内核(Kernel)创建一个进程。其次,加载可执行文件。(可执行文件是指Mach-O格式的文件,也就是App中所有.o文件的集合体)这时,能获取到dyld(dyld是苹果的动态链接器)的路径。
然后,加载dyld,主要分为4步:
1 . load dylibs:这一阶段dyld会分析应用依赖的dylib,找到其mach-o文件,打开和读取这些文件并验证其有效性,接着会找到代码签名注册到内核,最后对dylib的每一个segment调用mmap()。
2 . rebase/bind:进行rebase指针调整和bind符号绑定。
3 . ObjC setup:runtime运行时初始化。包括ObjC相关Class的注册、category注册、selector唯一性检查等。
4 . Initializers:调用每个ObjC类与分类的+load方法,调用attribute((constructor))修饰的函数、创建C++静态全局变量。
2.main函数执行后
这个阶段主要完成的是首屏的一些工作,包括首屏初始化的配置文件读取,列表数据的读取,渲染
总的来讲:main函数通知UIApplicationMain()去创建UIApplication(包括:代理,创建主RunLoop),然后读取plist文件,需不需要storyboard,在didFinish代理回调里设置Window,以及rootController
3.首屏渲染完成后
首屏渲染完成后的阶段,指的是:didFinishLaunchingWithOptions方法作用域内执行首屏渲染后的所有方法执行。即从设置self.window.rootViewController到didFinishLaunchWithOptions方法作用域结束。
启动优化
- 设置EnvionmentVariables :DYLD_PRINT_STATISTICS 设置为1(用于打印main函数调用之前,启动的各方面耗时)。DYLD_PRINT_STATISTICS_DETAILS : 可以打印得更详细
1.代码内减少使用+load方法,尽量将在+load内执行的内容放到渲染之后,或者使用+initialize()
2.动态库合并,苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持6个非系统动态库合并为一个。
3.优化类、方法、全局变量,减少加载启动后不会去使用的类或方法;少用C++全局变量
4.优化首屏渲染前的功能初始化,就是尽量不要把初始化放在didFinish
可执行文件(Mach-o)
- 相当于window的exe文件
- 在ipa包内体积最大那个就是了
- 可以使用MachoOView分析文件
动态链接库
库的链接分为静态和动态两种,iOS中直接影响到的就是包体积大小。1. 静态库,在我们开发完成之后会被打包到可执行文件内,包体积就大。2.动态库,比如UIKit系统库,存在于iOS系统内部,不被打包到我们的可执行文件内,只有在包安装到iOS上,启动时候会被dyld(动态链接器)链接起来,app就可以调用系统函数
Class原理
Class在iOS中实际上就是指向objc_class结构体的指针 | class、metaclass对象本质都是struct objc_class
- 1.实例对象(id):对类对象alloc或者new操作时创建,这个过程中会拷贝实例所属类的成员变量,但并不拷贝类定义的方法。实力对象在内存中存储的信息包括isa指针以及其他成员变量
- 2.类对象(class):使用class和object_getClass获取的对象都是类对象;每个类在内存中只有一份;类对象在内存中包括:isa指针、superclass指针,类的属性(property)、类的对象方法信息(instancemethod)、类的协议信息(protocol)、类的成员变量信息(ivar)
- 3.元类对象(metaclass):object_getClass([NSObject class])获取到的就是元类对象;每个类的内存有且只有一个meta-class对象;类方法存在于元类对象内;元类对象内包括:isa指针、superclass、类方法
- 4.[[NSObject class] class] 获取的是类对象不是元类对象
- runtime的class_isMetaClass可以查看是否元类对象
- 6.实例对象的isa指向类对象,类对象的isa指向元类对象
- 7.调用实例对象的对象方法时候,通过isa找到类对象,再调用类对象内的对象方法
- 8.调用类对象的类方法的时候,通过类对象的isa找到元类对象,再调用元类对象内的类方法
- 9.实例对象没有superclass
- 10.类对象的superclass指针指向父类的类对象
- 11.类对象superclass指针最终指向NSObject的类对象
- 12.实例对象要调用父类的对象方法时,需要使用isa找到自身的类对象,在用类对象的superclass指针找到父类的类对象,从而找到父类的对象方法。(找类方法差不多,就是找metaclass)
- 13.没有父类,superclas指针为nil
- 14.64bit开始,利用isa指针查找,需要&ISA_MASK 进行位运算才能计算真实的地址
- 15.class、metaclass对象本质都是struct objc_class
1 | //1. 弃用的Class指针结构体 |
KVO
- 全称 Key-Value Observing(‘键值监听’),可以用于监听某个对象属性值的改变
- KVO时,将被监听的对象isa指针动态修改成新类NSKVONotifying_Person
- 同时修改superclass和class两个实现,隐藏了NSKVONotifying_Person的存在,使用object_getClass可以找出来
- 通过set方法为属性赋值触发监听,也可以手动触发willcchange和didchange,直接修改成员变量不会触发监听
1 | - (void)setAge:(int)age |
KVC
- KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
Category
- 为已经存在的类添加方法、属性,无法添加成员变量;添加属性,只会生成set和get方法 (无法生成成员变量)
- Category 中的方法和类中原有方法同名,category 中的方法会覆盖掉类中原有的方法 (最后加载的留下)
- 使用关联变相给Category添加成员变量 设置:objc_setAssociatedObject,获取:objc_getAssociatedObject(self, &nameKey); - 分类添加属性
- 在Objective-C提供的runtime函数中,确实有一个lass_addIvar(),这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
- 实例对象在创建的时候就规定了isa指针以及成员变量,也就是实例变量的那块内存布局,如果动态添加了成员变量,就破坏了实例对象内存,这个实例对象就会变成了无效的对象。方法不存在实例对象内!
1 | //分类的源代码 |
AutoReleasePool
- 自动释放池 :一般理解就是自动帮OC对象添加release操作,从而合理管理引用计数
- 哨兵对象:NSAutoReleasePool 类的 push() 标记 一个哨兵对象 的内存地址,以NSAutoReleasePool成员变量的方式保存。AutoReleasePool代码块里面 创建 的OC对象,都会标记到哨兵对象之后的内存块中。(其实就是类似Array添加对象,然后在释放后把Array里面的所有对象都给释放掉一样),调用 [pool drain] 的时候,实际是调用NSAutoReleasePool 的pop方法,然后把添加在哨兵对象之后的OC对象全部释放
1 | //AutoReleasePoolPage的源码大概是这样的流程 |
+load方法
- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序:1先调用类的+load( 按照编译先后顺序调用,先编译,先调用;调用子类的+load之前会先调用父类的+load),2再调用分类的+load(按照编译先后顺序调用,先编译,先调用)
- +load方法不经过msgSend调用,而是直接使用函数地址
+load()与+initialize()两者的区别?
+load()方法会在main()函数调用前就调用,而+initialize()是在类第一次使用时才会调用。
+load方法的调用优先级: 父类 > 子类 > 分类,并且不会被覆盖,均会调用。
+load方法是在main() 函数之前调用,所有的类文件都会加载,包括分类也会加载。
+initialize方法的调用优先级:分类 > 子类,父类 > 子类。(父类的分类重写了+initialize方法会覆盖父类的+initialize方法)
+initialize方法
- 在类第一次接收到消息时调用
- 先调用父类的+initialize,再调用子类的+initialize (先初始化父类,再初始化子类,每个类只会初始化1次)
- initialize是通过objc_msgSend进行调用的
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用
符号表
ScrollView
- autolayout下系统会在viewDidAppear之前重新根据subView计算contentsize,所以在viewdidiload里面手动设置contentsize会被覆盖;希望在viewdidload下设置contentsize的话需要去掉autolayout选项,或者自己设置subView的constraint,或者在viewdidappear手动设置contentsize
View、UIWindow、CALayer
- View - UIResponder;CALayer - NSObject;
- 都是容器,View能响应事件,CALayer不能,UIWindow传递
frame 和 bounds
- frame指的是View在父View中的位置大小,参照物是父View的坐标系
- bounds指的是自身在坐标系中的位置大小,参照物是本身
关联对象ASSOCIATION
- 我们所说的关联对象的使用环境,或者说面试时候的关联对象,通常以给分类添加属性时候的使用,因为分类无法添加成员变量
- 设置: objc_setAssociatedObject(self, @”name”,name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 获取: objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); 移除: objc_removeAssociatedObjects(self);
- 关联对象并不是存储在被关联对象本身内存中
- 关联对象存储在全局的统一的一个AssociationsManager中
- 策略:
1 | typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { |
- 如果设置关联对象为nil,就相当于是移除关联对象
- weak修饰的属性,在对象销毁时候指针就置空了,但是哈希表内对应的值还在,当我们使用这个指针去释放的时候,这个指针地址已经为空了
Block
- block本质上也是一个OC对象,它内部也有个isa指针
- block是封装了函数调用以及函数调用环境的OC对象
- 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
- 定义block之后修改局部变量的值,在block调用的时候无法生效,局部变量传入block之后,内调用变量是在block内部的变量,所以外部修改局部变量无法生效
- block内部存在一个isa指针,表示这个结构体是一个oc对象
- 构造函数中传入的参数都存在了__main_block_impl_0这个结构体中,最后又将这个结构体的指针赋值给了block
- block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址
- Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存
1 | //clang编译器可以将oc代码装换成c++代码 |
Block捕获
Block内部可以正常访问外部变量,有一个捕获的机制
self同样被block捕获,不论对象方法还是类方法都会默认将self作为参数传递给方法内部
即使block中使用的是实例对象的属性,block中捕获的仍然是实例对象,并通过实例对象通过不同的方式去获取使用到的属性(self.name : 捕获self 通过name的get方法)
局部变量
auto变量
auto自动变量,离开作用域就销毁,通常局部变量前面自动添加auto关键字。
自动变量会被捕获到block内部,也就是说block内部会专门新增加一个参数来存储变量的值
auto只存在于局部变量中,访问方式为值传递,内存拷贝,深拷贝static变量
static 修饰的变量为指针传递,同样会被block捕获
捕获的内部是 int *age 的指针形式自动变量和静态变量的区别来自于销毁的机制:自动变量是入栈,函数结束等都可能被销毁;静态变量,内存中有且只有一份,不会被销毁。所以自动变量需要保存值,而静态变量保存指针地址就行
所以外部修改自动变量不影响block内部,修改静态变量就会影响
全局变量
- 全局变量哪里都能访问,所以__main_block_imp_0没有添加任何变量,不捕获任何变量
- 局部变量因为跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获
Block类型
- ARC环境下会对栈区的block进行一次copy操作,将block提升到堆区
- block继承于NSBlock,NSBlock继承于NSObject
- block的三种类型:1. NSGlobalBlock ( _NSConcreteGlobalBlock ),2.NSStackBlock ( _NSConcreteStackBlock ),3.NSMallocBlock ( _NSConcreteMallocBlock )
- NSGlobalBlock :不访问自动变量,多数情况下使用不到,通常全局变量在哪里都能访问,所以使用函数去完成相应的处理就行了
- NSStackBlock:访问自动变量,使用到最多的情况,捕获自动变量进栈
- NSMallocBlock:栈区的block在函数作用域结束时候跟着一起销毁,使用copy方式将block提升到堆区,防止内存被回收
block在内存中的分配区域
1 | // 验证代码:MRC环境下验证Block到底是进数据段还是堆还是栈 |
上述代码验证代码验证各个情况block的内存类型情况,验证结果
类型 | 环境 | 内存区域 |
---|---|---|
NSGlobalBlock | 没有访问auto变量 | 数据段 |
NSStackBlock | 访问oauto变量 | 栈区 |
NSMallocBlock | __NSStackBlock__调用copy | 堆区 |
各个类型的Block调用copy对类型的影响
类型 | 内存 | copy |
---|---|---|
NSGlobalBlock | 数据段 | 不改变类型,没反应 |
NSStackBlock | 栈区 | 由栈入堆,类型变NSMallocBlock |
NSMallocBlock | 堆去 | 增加引用计数,不改变类型 |
1 | //将block赋值给__strong指针时 |
_block修饰
- __block可以用于解决block内部无法修改auto变量值的问题,因为编译器会将_block包装成一个对象(_Block_object_assign),
- __block不能修饰全局变量、静态变量(static)
- __block变量从堆上移除会调用__block变量内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放指向的对象(release)
- 当__block变量在栈上时,不会对指向的对象产生强引用
_ __block变量被copy到堆时会调用__block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
_block修饰变量的实际表现:
Runtime
- 方法替换的实现,本质是类对象内rw_t内 methodlist方法数组内的方法对象的IMP交换掉
- 当使用method_exchange进行了函数交换,会清空缓存
- 替换方法时候需要重新将自生方法调用
在项目中的使用:
1.关联对象
2.遍历所有成员变量
3.归档解档
4.交换方法实现
5.利用消息转发修改报错,减少奔溃
Runloop
程序运行时候循环做事情
打个比方:runloop相当于流水线(线程)上的管家,等待我们需要的时候去做相应的操作,没活的时候,流水线改休息的休息(线程休眠),有活的时候就指挥流水线干活,合理分配流水线的工作(合理分配资源)
Runloop和线程的关系
每条线程都有唯一的一个与之对应的runloop对象
Runloop保存在一个全局的字典里面,线程为key,runloop为value
线程刚创建的时候没有runloop对象,Runloop会在第一次获取它时候创建
Runloop会在线程销毁的时候销毁
主线程的Runloop已经自动获取(创建),子线程默认不会开启runloop同一时间只能使用一个mode,
iOS 中使用了两套API控制Runloop : NSRunLoop 和 CFRunLoopRef
CFRunLoopRef是开源的,https://opensource.apple.com/tarballs/CF/
使用OC和C两种方式获取同一个Runloop的内存地址不同,原因是NSRunloop包装CFRunloopRef
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
RunLoop启动时只能选择其中一个Mode,作为currentMode
如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
1 | //获取runloop对象 |
1 |
|
执行流程
1 | 01、通知Observers:进入Loop |
- 关于线程保活问题
- 1.使用initwithtarget的循环引用
- 2.生命周期是否跟随控制器
多线程
iOS中的多线程方案
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 跨平台\可移植,通用的API | C | 程序猿管理 | 几乎不用 |
NSThread | 使用更加面向对象,使用简单 | OC | 程序猿管理 | 偶尔使用 |
GCD | 充分使用多核处理器,旨在替换NSThread | C | 自动管理 | 经常 |
NSOperation | 基于GCD,相比GCD更加简单 | OC | 自动管理 | 经常 |
队列的执行效果
同步 \ 异步 | 并发队列 | 手动创建串行队列 | 主队列 |
---|---|---|---|
同步(sync) | 没有开启新线程、串行执行 | 没有开启新线程、串行执行 | 没有开启新线程、串行执行 |
异步(async) | 开启新线程、并发执行 | 开启新线程、串行执行 | 没有开启新线程、串行执行 |
一般知识点
1.GCD源码:https://github.com/apple/swift-corelibs-libdispatch
2.注意死锁问题,造成死锁的关键在于:同步执行,并且在当前串行队列中添加任务就会造成死锁,即相互等待
3 .[self performSelector]的函数想要在子线程执行,需要启动子线程runloop,睡眠等该函数的执行,不然,在线程的任务一完成,子线程就会退出,这个时候执行performSelector的函数就会报错,提示该线程已经退出
4.多线程存在的安全隐患,多条线程同时操作块数据
5.cpu在执行多线程的时候就是使用时间片轮转调度算法
6.线程同步:不能让多条线程同时占用一份内存
7.GCD的常用函数:
1 | //用同步的方式执行任务: |
4个术语比较容易混淆:同步、异步、并发、串行
1.同步和异步主要影响:能不能开启新的线程
2.同步:在当前线程中执行任务,不具备开启新线程的能力
3.异步:在新的线程中执行任务,具备开启新线程的能力
4.并发和串行主要影响:任务的执行方式
5.并发:多个任务并发(同时)执行
6.串行:一个任务执行完毕后,再执行下一个任务
GNUstep
下载地址:http://gnustep.org/resources/downloads.php#core
用于参考Fundation框架
1 | runmode的原理就是不断的调用 |
线程组 - 可以在开发过程中完成不同任务的执行,同时执行,顺序执行等
1 | // 创建队列组 |
OSSpinLock 自旋锁
- #import <libkern/OSAtomic.h>
- iOS10之后不推荐使用,OSSpinLockUnlock’ is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock_unlock() from <os/lock.h>
- 等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
- 目前已经不再安全,可能会出现优先级反转问题,就是存在优先级比较高的线程在盲等状态, 导致cpu不会给优先级比较低的线程分配时间,这个线程比较低的锁就会卡住,产生类似死锁的情况
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
1 | @property (assign, nonatomic) OSSpinLock lock; |
os_unfair_lock
- 相比较OSSpinLock,不是盲等状态,而是让线程处于休眠状态,
1 | //初始化 |
pthread_mutex - 互斥锁
- 等待线程会处于睡眠状态
- 初始化属性传空表示默认 pthread_mutex_init(mutex, NULL);
- 关于属性为递归锁 PTHREAD_MUTEX_RECURSIVE:允许同一个线程对一把锁进行重复加锁
1 | #define PTHREAD_MUTEX_NORMAL 0 //默认 |
NSConditionLock - 条件锁
- 相比较NSCondition条件更加丰富
- [self.condition loclWhenCondition:1]; 当条件值为1的时候执行这个加锁,并且执行下去
- [self.condition unLoclWhenCondition:2]; 解锁,同时给这个锁一个为2的条件值
Semaphore - 信号量
- 控制并发访问的线程数量
`1
2
3
4
5
6
7
8
9
101.如果信号量的值大于0,进来减一
2.如果信号量 = 0,就会休眠等待
3.DISPATCH_TIME_FOREVER:一直等待,等待信号量大于0
4.DISPATCH_TIME_NOW:不等
self.mSemaphore = dispatch_semaphore_create(1);
//信号量等待,并发的信号量数取决于mSemaphore设置的大小
dispatch_semaphore_wait(self.mSemaphore, DISPATCH_TIME_FOREVER);
//信号量增加
dispatch_semaphore_signal(self.moneySemaphore);
对比几个锁的区别
- 自旋锁:wile等待,汇编代码,LLDB调式情况下,会重复调用一段代码,即wile等待
- pthread_mutex_lock:不使用的时候必须销毁,可以传入属性分别使用RECURSIVE递归锁或者默认锁
- pthread_cond:使用wait等待以及signal信号实现锁的等待和重新开启
- NSLock:pthread_mutex 的封装,OC形式的封装,
- NSRecursiveLock:pthread_mutex的递归锁的封装,OC的形式使用递归锁
- NSConditionLock:相当于包装了pthread_mutex和 pthread_cond 条件,遵守Locking协议,wait和signal配合使用
内存管理
atomic
- 给属性的set和get方法增加t原子性操作,也就是线程同步,也就是加锁和解锁 slotlock 和unslotlock
- 耗费性能
- 无法在持续使用的时候保证线程安全
读写安全,IO操作安全
- 读写锁 pthread_rwlock,自旋锁会等待
- dispatch_barrier_async 异步栅栏调用,在使用异步栅栏的时候传入的必须是并发队列,如果传入的是全局的并发或者串行队列的话,效果类似dispatch_async,就不是异步栅栏调用了
CADisplaylink NSTimer
- 1.使用target的形式创建的定时器,可能会造成循环引用,使用弱指针无法解决这个定时器的循环引用的问题(不同于block),target还是以参数的形式传入
- 2.使用block的形式创建的定时器,可以使用弱引用的方法来避免产生循环应用的问题
- 3.增加中间代理形式也可以避免产生循环应用,其中NSProxy就是用来完成这样的操作,其中使用消息转发的机制可以将函数给被代理方,
- 4.相比较NSObject做中间代理,完成消息转发的形式,NSProxy不需要到父类寻找方法,而是直接进入消息转发阶段,这样更加的高效
GCD定时器
- NSTimer 依赖于Runloop,如果runloop的任务过重会导致nstimer不准时
内存分布
- 从低到高、从上到下:1.保留,2.代码段(TEXT段),3.数据段(DATA)字符串常量,初始化和未初始化的全局变量和静态变量;4.堆区(heap);5.栈区(stack);6.内核区
- 代码段:编译后的代码;
- 数据段:1.字符串常量,2.已经初始化的全局变量和静态变量,3.未初始化的全局变量和静态变量;
- 堆:通过alloc,malloc,calloc等动态分配的内存空间,地址从低到高;
- 栈区:函数调用的开销(局部变量等),分配地址从高到低
Tagger Pointer
- 对Tagger Pointer查看引用计数为-1
- 可以存储NSDate、NSNumber、NSString等
- OC对象最小使用16个字节,OC指针8个字节,也就是说在没有Tagger Pointer技术之前,要存储一个NSNumber的值需要使用24个字节,而 int只需要4个字节就行,(减少浪费)
- OC对象存储:16个字节内存对齐,所以OC对象指针的最后一位存放0
- Tagger Pointer技术将数据直接存放在指针里面
- 编译器自行处理的
- 当Tagger Pointer的8个字节存储不下了,就存放在堆区
- 查找时候不需要像原来的调用,直接到指针内取值,节省了调用的开销
- 如果指针 p & 1<<63 = 1<<63 的话,这个对象就是一个Tagger Pointer指针,详情查看apple源码的判断是否Tagger Pointer。(mac平台判断最低有效位,iOS判断最高有效位)
- 总结使用了Tagger Pointer技术,存取都提高了效率
MRC
- 引用计数:新创建的OC对象引用计数默认为1
- Autorelease 自动释放,需要手动释放的mrc环境下,使用自动释放池,会在合适的时候将自动释放池内的内存进行释放
Copy、MutableCopy
- 原则:拷贝出来的对象修改不影响原对象
- 1.Copy拷贝出来的都是不可变,MutableCopy拷贝出来的都是可变
- 2.MutableCopy(不管对可变不可变对象进行拷贝的时候)都是深拷贝,新开辟一块内存存储
- 3.Copy对可变对象拷贝的时候,深拷贝,会生成一个不可变的对象,新的不可变内存
- 4.特殊情况:Copy对不可变对象拷贝的时候:浅拷贝,指针拷贝,生成新指针指向同一块内存,且copy引用计数会加一;原因是对不可变对象Copy出来的还是不可变对象,既然是不可变对象不能修改,就不生成新的对象浪费内存
- 5.属性中使用copy,表示就是将来新传进来的对象就是进行一次copy操作,所以属性只要用得copy关键字,就不要使用可变对象,后期使用可变的函数会报错,方法不存在
- 6.实现copy协议可以使对象可以copy
引用计数
- oc源码:NSObject.mm内 ratinCount函数,先判断isa是否64位新指针,再判断是否存在SideTqbles中的RefcountMap(哈希表)
- 对象的引用计数存放在对象isa指针结构体得最后一个数(19位),如果19位不够存储,则将改引用计数存放在isa指针内的一个SideTqbles中(散列表结构)
- retain加一,release减一
- 对象引用计数减为0时候msgsend 发送dealloc消息销毁对象
weak指针
- isa指针内的SideTable存在一个表,用于存放weak指针的weak_table_t(哈希表)中
- 在对象销毁时会调用clearDeallocating函数找到存放弱指针的哈希表,清除弱引用
ARC
- LLVM编译器会在合适得地方自动生成 release,autorelease代码
- Runtime处理弱引用等操作
- 编译器对局部对象在当前函数最后添加release,对象会在函数执行完毕时候就释放
AutoReleasePool自动释放池
- 存在构造函数和析构函数,构造函数会调用push,析构函数会调用pop
- 在释放池内得函数相当于夹在了释放池的push和pop两个函数之间
- 释放池会在push()的时候入栈一个POOL_BOUNDAY(边界),在pop()结束的时候将这个边界的地址值传入,从最后开始Release,直到遇到这个边界表示释放完毕
- 每个@AutoReleasePool回调一次push,每次push都会加一个边界
- 查看自动释放池情况的函数(不开源的): 1.声明 extern void _objc_autoreleasePoolPrint(void); 2. 使用 _objc_autoreleasePoolPrint();
- 释放时机:执行release是由runloop决定的,释放池会在runloop休眠时执行pop完成release操作
性能优化
卡顿
- 将cpu和gpu处理的时间控制在16毫秒内,保证60fps的帧频
- 尽量使用轻量级对象
- 不要频繁使用View的相关属性,尽量避免不必要的修改
- 提前计算好布局,在需要时一次性修改
- Autolayout会比直接设置frame更消耗cpu资源
- 控制线程并发数量
- 图片得size最好跟UIImageView的大小保持一致
- 耗时操作放入子线程(文本绘制,图片解码在子线程解码)
- 减少视图层次
- 减少透明的View
- 避免离屏渲染(光栅化:layer.shouldRasterize = yes, 遮罩,圆角,阴影)
监控卡顿
- 检测runloop 处理source的时间达到监控卡顿的效果
电量
- 降低CPU、GPU的消耗
- 尽量少用定时器
- 优化文件操作
- iOS提供了基于GCD得一部操作API,dispatch_io,优化了磁盘访问
- 压缩网络数据
- 尽量避免实时定位
- 只需要一次获取位置可以使用requestLocation,该函数会调用一次定位获取用户位置信息,用完之后就对定位的硬件进行断电
- 如果实时要求较高的,可以选择合适的定位精度,精度越高调用频率越高,耗电就越快